package hhpoll.service;

import hhpoll.dao.DAOFactory;
import hhpoll.dao.PollDAO;
import hhpoll.dao.UserDAO;
import hhpoll.domain.Poll;
import hhpoll.domain.PollItem;
import hhpoll.domain.PollParticipant;
import hhpoll.domain.User;
import hhpoll.service.context.RequestContext;
import hhpoll.validation.PollValidator;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.log4j.Logger;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.springframework.mail.MailSender;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.BindException;

@Transactional
public class PollServiceImpl implements PollService {
	private Logger log = Logger.getLogger(getClass());
	
	private DAOFactory daoFactory;
	private UserService userService;
	private MailSender mailSender;
	private SimpleMailMessage invitationMessage;
	
	public PollServiceImpl (DAOFactory daoFactory, UserService userService, 
			                MailSender mailSender, SimpleMailMessage invitationMessage) {
		this.daoFactory = daoFactory;
		this.userService = userService;
		this.mailSender = mailSender;
		this.invitationMessage = invitationMessage;
	}

	@Override
	public Poll createPoll(RequestContext ctxt, Poll poll) {
		if (ctxt == null)
			throw new ServiceException("Authentication/request context is required");
		
        // Authenticate the user based on the RequestContext
        User theUser = userService.authenticate(ctxt.getUsername(), ctxt.getPassword());
        
        // List to hold new users.
        List<String> newUserList = new ArrayList<String>();
        
		try {
			// Basic properties
			poll.setCreateDate(new Date());
			poll.setAdministrator(theUser);
			// process participants collection
			if (poll.getParticipants() != null) {
    			log.info(poll.getParticipants().size()+" Participants to create for poll");
    			for (PollParticipant participant : poll.getParticipants()) {
    				log.info("Participant: email="+participant.getEmail());
                	// Set basic fields on the participant
                	participant.setId(null);
                	participant.setVote(null);
                	participant.setVoteDate(null);
    				if (!applyParticipantUser(participant))
    					newUserList.add(participant.getEmail());
    			}
			}
			// process items collection
			if (poll.getItems() != null) {
    			log.info(poll.getItems().size()+" Items to create for poll");
    			for (PollItem pollItem: poll.getItems()) {
    				log.info("Item: text="+pollItem.getText());
    				pollItem.setId(null);
    				pollItem.setPollId(null);
    				pollItem.setNumVotes(0);
    				// The user should be set to the administrator, since they are defining the poll
    				pollItem.setCreator(theUser);
    				pollItem.setCreateDate(new Date());
    			}
			}
			// Create the poll.
			PollDAO pollDao = this.daoFactory.createPollDAO();
			pollDao.create(poll);
			
			// Now we can send e-mail to all the participants.
			try {
    			for (PollParticipant participant : poll.getParticipants()) {
    				SimpleMailMessage message = new SimpleMailMessage(invitationMessage);
    				message.setTo(participant.getEmail());
    				message.setSubject("Invitation to poll: "+poll.getTitle());
    				message.setSentDate(new Date());
    				mailSender.send(message);
    			}
			} catch (Exception e) {
				log.error("Unable to send mail", e);
			}
		} catch (HibernateException e) {
			throw new ServiceException("Unable to create poll: "+e.getMessage(), e);
		}
		return poll;
	}
	
	@Override
	public Poll getPoll(RequestContext context, Long pollId, boolean participantDetails, boolean itemDetails) {
		if (context == null)
			throw new ServiceException("Authentication/request context is required");
		
		// TODO: user can only get a poll in which they are an administrator, participant 
		// or that is public.
		
		try {
			PollDAO pollDao = this.daoFactory.createPollDAO();
			Poll poll = pollDao.findByID(pollId);
			if (poll == null)
    			throw new ServiceException("Poll not found: "+pollId);
			
			// The following should create a detached instance, given the
			// persistent instance returned by Hibernate.  This is kind of nasty,
			// but if we don't create a detached instance, then if we set the "items"
			// or "participants" collections to null, it will cause the change to be
			// cascaded to the DB when the Hibernate session is flushed.  We possibly
			// want to return empty sets for these collections, if the user did not
			// request to retrieve details.
			Poll retpoll = new Poll();
			retpoll.setPublc(poll.getPublc());
			retpoll.setAdministrator(poll.getAdministrator());
			retpoll.setAnonymous(poll.getAnonymous());
			retpoll.setCreateDate(poll.getCreateDate());
			retpoll.setDynamic(poll.getDynamic());
			retpoll.setEndDate(poll.getEndDate());
			retpoll.setId(poll.getId());
			if (itemDetails) {
				log.info("itemDetails is set: initializating collection");
				Hibernate.initialize(poll.getItems());
    			retpoll.setItems(poll.getItems());
			}
			if (participantDetails) {
				log.info("participantDetails is set: initializating collection");
				Hibernate.initialize(poll.getParticipants());
    			retpoll.setParticipants(poll.getParticipants());
    			for (PollParticipant pp : poll.getParticipants())
    				applyParticipantUser(pp);
			}
			retpoll.setTitle(poll.getTitle());
			retpoll.setStartDate(poll.getStartDate());
			
			return retpoll;
		} catch (HibernateException e) {
			throw new ServiceException("Unable to get poll: "+e.getMessage(), e);
		}
	}


	@Override
	public PollParticipant vote(RequestContext ctxt, Long pollId, Long pollItemId) {
		if (ctxt == null)
			throw new ServiceException("Authentication/request context is required");
		
		try {
			// Make sure user is authenticated first.
			User user = userService.authenticate(ctxt.getUsername(), ctxt.getPassword());
			log.info("User authenticated successfully");
			
			// Get the poll from the database
			PollDAO pollDao = this.daoFactory.createPollDAO();
			Poll poll = pollDao.findByID(pollId);
			
			// Ensure this poll exists
			if (poll == null)
				throw new ServiceException("Unable to find poll: "+pollId);
			
			// Ensure that the poll is currently active.
    		PollValidator validator = new PollValidator();
    		BindException errors = new BindException(poll, "poll");
    		validator.validateActive(poll, errors);
    		if (errors.hasErrors()) {
    			// XXX: Need to construct a better message based on the errors object.
    			throw new ServiceException(errors.getMessage());
    		}
			
			// Find the item in the poll ; throw an error if not found.
			PollItem pollItem = null;
			for (PollItem item: poll.getItems())
				if (pollItemId.equals(item.getId())) {
					pollItem = item;
					log.info("Found corresponding poll item: "+pollItem.getText());
					break;
				}
			
			if (pollItem == null)
				throw new ServiceException("Unable to find poll item: "+pollItemId);
			
			// Ensure this poll item belongs to the poll the user passed in
			if (pollItem.getPollId() == null || pollItem.getPollId() != pollId)
				throw new ServiceException("Item \""+pollItem.getText()+"\" is not part of poll: "+poll.getTitle());
			
			// Check if the user is found as an existing participant
			PollParticipant pp = null;
			log.info("Looking for match for email="+user.getEmail());
			for (PollParticipant pollPart : poll.getParticipants()) {
				log.info("pollPart.getEmail()="+pollPart.getEmail());
				if (!user.getEmail().equals(pollPart.getEmail()))
					continue;
				
				log.info("Found existing participant: "+user.getEmail());
				pp = pollPart;
				
				if (pp.getVote() == null) {
					log.info("First time this participant has voted; incrementing numVotes for this item");
					pollItem.setNumVotes(pollItem.getNumVotes()+1);
				} else if (!pp.getVote().getId().equals(pollItem.getId())) {
					log.info("Participant changing their vote");
					for (PollItem item : poll.getItems())
						if (pp.getVote().getId().equals(item.getId())) {
							item.setNumVotes(item.getNumVotes()-1);
							break;
						}
					pollItem.setNumVotes(pollItem.getNumVotes()+1);
				} else {
					log.info("Participant voted for the same item again!");
				}
				
				pp.setVote(pollItem);
				pp.setVoteDate(new Date());
				applyParticipantUser(pp);
			}
			
			// If no user found, but the poll is public, then allow them to vote 
			// and add them as a participant.  If the poll is private, they can only vote
			// if they are the administrator of the poll.
			if (pp == null) {
				boolean isAdministrator = user.getEmail().equals(poll.getAdministrator().getEmail());
				if (!isAdministrator && !poll.getPublc())
    				throw new ServiceException("You can't vote in a private poll in which you are not a participant");
				log.info("Unable to find poll participant, creating: "+user.getEmail());
				pp = new PollParticipant();
				pp.setPollId(poll.getId());
				pp.setEmail(user.getEmail());
				pp.setUser(user);
				pp.setVote(pollItem);
				pp.setVoteDate(new Date());
				poll.getParticipants().add(pp);
				
				// Increment the numVotes for this item.
				pollItem.setNumVotes(pollItem.getNumVotes()+1);
			}
			
			log.info("Updating poll");
			pollDao.update(poll);
			return pp;
		} catch (HibernateException e) {
			throw new ServiceException("Unable to get poll: "+e.getMessage(), e);
		}
	}

	@Override
	public PollParticipant addParticipant(RequestContext ctxt, String email, Long pollId) {
		if (ctxt == null)
			throw new ServiceException("Authentication/request context is required");
		if (email == null)
			throw new ServiceException("E-mail address is required to add a participant.");
		if (pollId == null)
			throw new ServiceException("PollId is required to add a participant");
		
        // Authenticate the user based on the RequestContext
        User theUser = userService.authenticate(ctxt.getUsername(), ctxt.getPassword());
        
        try {
        	// First, get the poll from the database.
        	PollDAO pollDao = daoFactory.createPollDAO();
        	Poll poll = pollDao.findByID(pollId);
        	if (poll == null)
        		throw new ServiceException("Unable to find pollId="+pollId);
        	
			// Ensure that the poll is currently active.
    		PollValidator validator = new PollValidator();
    		BindException errors = new BindException(poll, "poll");
    		validator.validateActive(poll, errors);
    		if (errors.hasErrors()) {
    			// XXX: Need to construct a better message based on the errors object.
    			throw new ServiceException(errors.getMessage());
    		}
        	
        	// Only the administrator can add new participants
        	if (!theUser.equals(poll.getAdministrator()))
        		throw new ServiceException("Only the administrator can add new participants");
        	
            // Check the poll to see if this user is already a participant.
            for (PollParticipant pp : poll.getParticipants()) {
            	if (email.equals(pp.getEmail()))
            		throw new ServiceException(email+" is already a participant in pollId="+pollId);
            }
            PollParticipant participant = new PollParticipant();
        	// Set basic fields on the participant
        	participant.setId(null);
        	participant.setVote(null);
        	participant.setVoteDate(null);
            participant.setEmail(email);
            participant.setPollId(pollId);
            applyParticipantUser(participant);
            
            log.info("Adding new participant: "+participant.getEmail());
            poll.getParticipants().add(participant);
            pollDao.update(poll);
            
            try {
    			// Now we can send an e-mail to the participant.
    			SimpleMailMessage message = new SimpleMailMessage(invitationMessage);
    			message.setTo(participant.getEmail());
    			message.setSubject("Invitation to poll: "+poll.getTitle());
    			message.setSentDate(new Date());
    			mailSender.send(message);
            } catch (Exception e) {
            	log.error("Unable to send mail", e);
            }
            
            return participant;
        } catch (HibernateException e) {
        	throw new ServiceException ("Unable to add participent"+e.toString(), e);
        }
	}
	
	@Override
	public PollItem addItem(RequestContext ctxt, String text, Long pollId) {
		if (ctxt == null)
			throw new ServiceException("Authentication/request context is required");
		if (text == null)
			throw new ServiceException("Item text is required to add an item");
		if (pollId == null)
			throw new ServiceException("PollId is required to add an item");
		
        // Authenticate the user based on the RequestContext
        User theUser = userService.authenticate(ctxt.getUsername(), ctxt.getPassword());
        
        try {
        	// First, get the poll from the database.
        	PollDAO pollDao = daoFactory.createPollDAO();
        	Poll poll = pollDao.findByID(pollId);
        	if (poll == null)
        		throw new ServiceException("Unable to find pollId="+pollId);
        	
			// Ensure that the poll is currently active.
    		PollValidator validator = new PollValidator();
    		BindException errors = new BindException(poll, "poll");
    		validator.validateActive(poll, errors);
    		if (errors.hasErrors()) {
    			// XXX: Need to construct a better message based on the errors object.
    			throw new ServiceException(errors.getMessage());
    		}
            	
        	// Only the administrator can add new items for a non-dynamic poll
        	if (!poll.getDynamic() && !theUser.equals(poll.getAdministrator()))
        		throw new ServiceException("Only the administrator can add new items for a static poll");
        	
            // Check the poll to see if this item already exists.
            for (PollItem pollItem: poll.getItems()) {
            	if (text.equals(pollItem.getText()))
            		throw new ServiceException("Item \""+text+"\" already exists for pollId="+pollId);
            }
            
            PollItem pollItem = new PollItem();
        	// Set basic fields on the participant
        	pollItem.setId(null);
        	pollItem.setText(text);
        	pollItem.setNumVotes(0);
        	pollItem.setCreateDate(new Date());
        	pollItem.setCreator(theUser);
            pollItem.setPollId(pollId);
                
            log.info("Adding new item: "+text);
            poll.getItems().add(pollItem);
            pollDao.update(poll);
                
            return pollItem;
        } catch (HibernateException e) {
        	throw new ServiceException ("Unable to add item"+e.toString(), e);
        }
	}
	
	@Override
	public List<Poll> findMyPolls(RequestContext ctxt, Date start, Date end) {
		if (ctxt == null)
			throw new ServiceException("Authentication/request context is required");
		
        // Authenticate the user based on the RequestContext
        userService.authenticate(ctxt.getUsername(), ctxt.getPassword());
        
        List<Poll> myPolls = new ArrayList<Poll>();
        try {
        	// First, get the poll from the database.
        	PollDAO pollDao = daoFactory.createPollDAO();
        	List<Poll> retPolls;
        	if (start != null && end != null)
            	retPolls = pollDao.findActiveByAdministrator(start, end, ctxt.getUsername());
        	else
            	retPolls = pollDao.findByAdministrator(ctxt.getUsername());
        	
        	for (Poll poll : retPolls) {
        		Poll copyPoll = new Poll();
        		copyPoll.setAdministrator(poll.getAdministrator());
        		copyPoll.setAnonymous(poll.getAnonymous());
        		copyPoll.setCreateDate(poll.getCreateDate());
        		copyPoll.setDynamic(poll.getDynamic());
        		copyPoll.setEndDate(poll.getEndDate());
        		copyPoll.setId(poll.getId());
        		copyPoll.setPublc(poll.getPublc());
        		copyPoll.setStartDate(poll.getStartDate());
        		copyPoll.setTitle(poll.getTitle());
        		myPolls.add(copyPoll);
        	}
            	
            return myPolls;
        } catch (HibernateException e) {
        	throw new ServiceException ("Unable to add item"+e.toString(), e);
        }
	}

	@Override
	public List<Poll> findParticipantPolls(RequestContext ctxt, Date start, Date end) {
		if (ctxt == null)
			throw new ServiceException("Authentication/request context is required");
		
        // Authenticate the user based on the RequestContext
        userService.authenticate(ctxt.getUsername(), ctxt.getPassword());
        
        List<Poll> participantPolls = new ArrayList<Poll>();
        try {
        	// First, get the poll from the database.
        	PollDAO pollDao = daoFactory.createPollDAO();
        	List<Poll> retPolls;
        	if (start != null && end != null)
            	retPolls = pollDao.findActiveByParticipant(start, end, ctxt.getUsername());
        	else
            	retPolls = pollDao.findByParticipant(ctxt.getUsername());
        	
        	for (Poll poll : retPolls) {
        		Poll copyPoll = new Poll();
        		copyPoll.setAdministrator(poll.getAdministrator());
        		copyPoll.setAnonymous(poll.getAnonymous());
        		copyPoll.setCreateDate(poll.getCreateDate());
        		copyPoll.setDynamic(poll.getDynamic());
        		copyPoll.setEndDate(poll.getEndDate());
        		copyPoll.setId(poll.getId());
        		copyPoll.setPublc(poll.getPublc());
        		copyPoll.setStartDate(poll.getStartDate());
        		copyPoll.setTitle(poll.getTitle());
        		participantPolls.add(copyPoll);
        	}
            	
            return participantPolls;
        } catch (HibernateException e) {
        	throw new ServiceException ("Unable to add item"+e.toString(), e);
        }
	}


	@Override
	public List<Poll> findPublicPolls(RequestContext ctxt, Date start, Date end) {
		if (ctxt == null)
			throw new ServiceException("Authentication/request context is required");
		
        // Authenticate the user based on the RequestContext
        userService.authenticate(ctxt.getUsername(), ctxt.getPassword());
        
        List<Poll> publicPolls = new ArrayList<Poll>();
        try {
        	// First, get the poll from the database.
        	PollDAO pollDao = daoFactory.createPollDAO();
        	List<Poll> retPolls;
        	if (start != null && end != null)
            	retPolls = pollDao.listActivePublic(start, end);
        	else
            	retPolls = pollDao.listPublic();
        	
        	for (Poll poll : retPolls) {
        		Poll copyPoll = new Poll();
        		copyPoll.setAdministrator(poll.getAdministrator());
        		copyPoll.setAnonymous(poll.getAnonymous());
        		copyPoll.setCreateDate(poll.getCreateDate());
        		copyPoll.setDynamic(poll.getDynamic());
        		copyPoll.setEndDate(poll.getEndDate());
        		copyPoll.setId(poll.getId());
        		copyPoll.setPublc(poll.getPublc());
        		copyPoll.setStartDate(poll.getStartDate());
        		copyPoll.setTitle(poll.getTitle());
        		publicPolls.add(copyPoll);
        	}
            	
            return publicPolls;
        } catch (HibernateException e) {
        	throw new ServiceException ("Unable to add item"+e.toString(), e);
        }
	}
	
	/**
	 * Sets the user on the PollParticipant passed in, if the user already exists in the
	 * system.  Otherwise, returns false to indicate that the e-mail provided corresponds
	 * to a new user.
	 * @param participant
	 * @param pollId
	 * @return
	 */
	private boolean applyParticipantUser (PollParticipant participant) {
		boolean userFound = false;
		try {
        	// Next, check if there is an existing user for the corresponding e-mail address
            UserDAO userDao = daoFactory.createUserDAO();
            User user = userDao.findByEmail(participant.getEmail());
            if (user != null) {
            	participant.setUser(user);
            	userFound = true;
            }
		} catch (HibernateException e) {
        	throw new ServiceException ("applyParticipantUser", e);
		}
		
		return userFound;
	}


}
